#!/usr/bin/env python
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# $Id: qroxy-local.py 2 2010-03-26 06:14:44Z qiaolun $
#


import sys
import BaseHTTPServer
import httplib, urllib, urlparse
import logging
import simplejson as json
import base64
import socket
import SocketServer
import re
import select
import GenericCache

LOCAL_HOST = ''
LOCAL_PORT = 8205

#ner
GATE_NETLOC_URL = "http://www.procentai.lt:80/p.php"
#GATE_NETLOC = "upe.appspot.com:80"

CACHE_CONTENT_TYPE = ('text/css', 'application/x-javascript','text/html','text/javascript','image/gif','image/jpeg','image/png')

FORBIDDEN_HOSTS = ('www.google-analytics.com'
        ,'.cnzz.com' 
        ,'.51.la'
        ,'.51yes.com'
        ,'.linezing.com')

MAX_CACHE_SIZE = 9999
cache = GenericCache.GenericCache(maxsize=MAX_CACHE_SIZE, expiry=900)
server_status = {'cache_hit':0
        ,'cache_miss':0
        ,'requests':0}

socket.setdefaulttimeout(60)

from datetime import datetime
from email.utils import parsedate

def _parse_http_datetime(s):
    return datetime(*parsedate(s)[:6])


class QroxyHTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):

    def do_HEAD(self):
        self._do_request('HEAD', self.path)

    def do_GET(self):
        self._do_request('GET', self.path)

    def do_POST(self):
        self._do_request('POST', self.path)

    def do_CONNECT(self):
        """ https request
        {{{
        """
        print "CONNECT %s" % (self.path,)

        (httpsHost, _, httpsPort) = self.path.partition(':')
        if httpsPort != '' and httpsPort != '443':
            self.send_error(501, 'Local proxy error, Only port 443 is allowed for https.')
            self.connection.close()
            return

        self.wfile.write('HTTP/1.1 200 OK\r\n')
        self.wfile.write('\r\n')

        """
        import ssl
        sslSock = ssl.SSLSocket(self.connection,
                                server_side=True,
                                keyfile="host.key",
                                certfile="host.cert")
        """
        
        from OpenSSL import SSL

        ctx = SSL.Context(SSL.SSLv23_METHOD)
        # ctx.set_options(SSL.OP_NO_SSLv2)
        # ctx.set_verify(SSL.VERIFY_PEER|SSL.VERIFY_FAIL_IF_NO_PEER_CERT, verify_cb) # Demand a certificate
        ctx.use_privatekey_file ('server.pkey')
        ctx.use_certificate_file('server.cert')

        ssl_conn = SSL.Connection(ctx, self.connection)
        ssl_conn.set_accept_state()
        ssl_conn.setblocking(0)

        """
        while True:
            try:
                ssl_conn.do_handshake()
                break
            except SSL.WantReadError:
                continue
            except KeyboardInterrupt:
                break
            else:
                break
        """
        

        ssl_in = ''

        # read header
        content_length_left = 0
        while True:
            try:
                line = self.readline(ssl_conn)
            except (SSL.WantReadError, SSL.WantWriteError, SSL.WantX509LookupError):
                pass
            except SSL.ZeroReturnError:
                ssl_conn.close()
                self.connection.close()
                return
            except SSL.Error, errors:
                ssl_conn.close()
                self.connection.close()
                return
            else:
                ssl_in += line
                # print line
                if line.startswith('Content-Length:'):
                    content_length_left = int(line[15:].strip())
                
                if line == "\r\n":
                    # no body, request finished
                    break
       
        # read body
        while content_length_left > 0:
            try:
                buf = ssl_conn.recv(8192)
            except (SSL.WantReadError, SSL.WantWriteError, SSL.WantX509LookupError):
                pass
            except SSL.ZeroReturnError:
                ssl_conn.close()
                self.connection.close()
                return
            except SSL.Error, errors:
                ssl_conn.close()
                self.connection.close()
                return
            else:
                ssl_in += buf
                buf_len = len(buf)
                content_length_left -= buf_len


        # print ssl_in
        first_line, other_lines = ssl_in.split("\r\n", 1)

        # get path
        (method, path, ver) = first_line.split()
        if path.startswith('/'):
            path = 'https://%s' % httpsHost + path


        print "Forward %s %s %s" % (method, path, ver)

        # connect to local proxy server
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect(('127.0.0.1', 8205))
        sock.send('%s %s %s\r\n' % (method, path, ver))
        sock.send(other_lines)

        # response
        while True:
            r,w,e = select.select([sock],[],[])
            if not r:
                continue

            buf = sock.recv(8192)
            if not buf:
                break

            ssl_conn.send(buf)



        # clean
        sock.close()
        ssl_conn.shutdown()
        ssl_conn.close()
        self.connection.close()
        """ }}} """
    
    def _do_request(self, method, path):
        """ http request
        {{{
        """
        
        server_status['requests'] += 1

        data = None

        # test host
        up = urlparse.urlparse(path)
       
        # check direct access
        if not up.scheme and not up.hostname:
            # direct access
            data = self._handle_request()

        # start proxy routine
        hit_cache = None
        
        if not data and not up.hostname:
            # bad host name , over
            data = self._error_data(500, 'bad host name')
      
        if not data and up.hostname.endswith(FORBIDDEN_HOSTS):
            # forbidden
            data = self._error_data(500, 'host forbidden')

        cache_control = self.headers.get('cache-control')
        if not data and cache_control != 'max-age=0':
            # try cache
            data = cache[path]
            if data:
                hit_cache = 'hit'
                server_status['cache_hit'] += 1

        if not data:
            data = self._do_fetch_data(method, path)
#ner
            if  data['status'] != 401:
                self._check_save_cache(method, path, data)
                hit_cache = 'miss'
                server_status['cache_miss'] += 1

        # output resposne 
        self.send_response2(data.get('status'))
        for name,value in data.get('headers'):
            if name == 'set-cookie':
                pattern = r',\s(?=[^\d]{1})'
                values = re.split(pattern, value)
                for cookie_value in values:
                    self.send_header(self._cap_header_key(name), cookie_value)
            else:
                self.send_header(self._cap_header_key(name), value)
            
        if hit_cache:
            self.send_header("X-Qroxy-Cache", hit_cache)

        self.end_headers()
        self.log_message("Cache %s: %s", hit_cache, path)

        self.wfile.write(data.get('body'))

        self.connection.close()
        """ }}} """

    def _do_fetch_data(self, method, path):
        """ fetch remote data
        {{{
        """
        # print "%s %s" % (method, path)

        params = {}

        # pass browser headers
        for name in self.headers:
            value = self.headers.get(name)
            params['header-' + name] = self._encode(value)

        params['qroxy-method'] = self._encode(method)
        params['qroxy-url'] = self._encode(path)

        if method == 'POST':
            len = int(self.headers.get('content-length'))
            params['qroxy-postdata'] = self._encode(self.rfile.read(len))
        else:
            params['qroxy-postdata'] = ''


        param_string = urllib.urlencode(params)
        
        headers = {"Content-Type":"application/x-www-form-urlencoded", "Accept-Encoding":"gzip", "User-Agent":"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.2) Gecko/20100115 Firefox/3.6"}

        data = self._error_data(500, 'init')

        try:
#ner
            url2 = urlparse.urlparse(GATE_NETLOC_URL)
            url2path = url2.path
            url2loc = url2.hostname + ':' + str(url2.port)
            conn = httplib.HTTPConnection(url2loc)
            conn.request("POST", url2path, param_string, headers)
#            conn.request("POST", "/zzz", param_string, headers)
            response = conn.getresponse()
            # print response.status, response.reason
            gate_status = response.status
            gate_body = response.read()
            gate_headers = response.getheaders()
            gate_encoding = response.getheader('Content-Encoding', None)
            conn.close()

            # print response_headers

            if gate_encoding == 'gzip':
                # encoding between qroxy-local and qroxy-gate
                # print "using gzip."
                import StringIO
                compressedstream = StringIO.StringIO(gate_body)
                import gzip
                gzipper = gzip.GzipFile(fileobj=compressedstream)
                gate_body = gzipper.read() 

            data = json.loads(gate_body)
            data['body'] = base64.b64decode(data['body'])
        except json.JSONDecodeError:
            data = self._error_data(500, 'decode error')
        except socket.timeout:
            data = self._error_data(504, 'timeout')

        return data
        """ }}} """

    def _handle_request(self):
        if self.path == '/status':
            t = '<p>'
            t += "Requests: %d <br/>" % (server_status['requests'],)
            t += "Cache: %d Hit, %d Miss <br />" % (server_status['cache_hit'], server_status['cache_miss'])
            t += "Cache Size: %d / %d <br/>" % (len(cache), MAX_CACHE_SIZE)
            t += '</p>'

            data = self._error_data(200, "Cache Status", t)
        else:
            data = self._error_data(404, 'not found')

        return data

    def readline(self, s):
        line = ''
        while True:
            chr = s.recv(1)
            if chr == '':
                # error
                return None
            if chr == '\r':
                chr = s.recv(1)
                if chr == '\n':
                    line += '\r\n'
                    break
                else:
                    # bad request
                    return None
            # newline(\n)?
            if chr == '\n':
                line += '\r\n'
                break
            line += chr

        return line


    def _check_save_cache(self, method, path, data):

        use_cache = True
        if method != 'GET':
            use_cache = False

        page_date = None
        page_expire = None

        for name,value in data.get('headers'):
                    
            # decide if use cache
            if name == 'content-type':
                check_ct = False
                for ct in CACHE_CONTENT_TYPE:
                    if value.startswith(ct):
                        check_ct = True
                        break
                use_cache &= check_ct
            elif name == 'pragma' and value == 'no-cache':
                use_cache = False
            elif name == 'cache-control':
                cc = value.split(',')
                for cci in cc:
                    if cci.strip() in ['private','no-store','no-cache','must-revalidate']:
                        use_cache = False
                        break

            elif name == 'date':
                page_date = value
            elif name == 'expires':
                page_expire = value

        if page_date and page_expire:
            try:
                t_date = _parse_http_datetime(page_date)
                t_expire = _parse_http_datetime(page_expire)
                if t_date > t_expire:
                    use_cache = False
            except:
                pass

        if use_cache:
            # print "SaveCache: %s" % (path,)
            cache[path] = data



    def _encode(self, value):
        return base64.b64encode(value)

    def _error_data(self, code, message, body=''):
        body = """
        <html>
        <head><title>Qroxy</title></head>
        <body>
        <h1>
        Qroxy: """ + message.title() + """
        </h1>
        """ + body + """
        <hr />
        <address>$Id: qroxy-local.py 2 2010-03-26 06:14:44Z qiaolun $</address>
        </body></html>
        """
        return {'status':code,'headers':[('content-type','text/html')], 'body':body}

    def send_response2(self, code, message=None):
        """Send the response header and log the response code.

        """
        self.log_request(code)
        if message is None:
            if code in self.responses:
                message = self.responses[code][0]
            else:
                message = ''
        if self.request_version != 'HTTP/0.9':
            self.wfile.write("%s %d %s\r\n" %
                             (self.protocol_version, code, message))



    def _cap_header_key(self, key):
        return '-'.join((ck.capitalize() for ck in key.split('-')))


class ThreadingHTTPServer(SocketServer.ThreadingMixIn,
                              BaseHTTPServer.HTTPServer):
    pass

def run(server_class=ThreadingHTTPServer,
    handler_class=QroxyHTTPHandler):
    """
    run server
    """
    print "Start Server."

    server_address = (LOCAL_HOST, LOCAL_PORT)
    httpd = server_class(server_address, handler_class)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    httpd.server_close()

    print "Stop Server."
    sys.exit(1)


if __name__ == '__main__' :
    run()

